flutter - 数据流

flutter - 数据流

前言

前不久的谷歌 io 上有几场关于 flutter 的演讲,其中 Build reactive mobile apps with Flutter 的一场主题演讲令我印象深刻。它简单易懂的讲述了一个 flutter app 该如何实现界面与数据和状态的绑定,这里希望可以通过这篇文章记录一下学习成果。我也在 github 上找到了该演讲演示的项目代码 state_experiments,当然你如果看到这篇文章,我希望你能有一些 flutter 的基础

setState() 方法

flutter 是一种响应式的界面编程,数据与视图分离,原理与 react 比较相似。在外部会构建出 Widget Tree(开销很小,每次都会重建),进而会在内部构建出 Render Tree(其实中间还有一个 Element Tree,这个新建销毁的开销相对较大,所以会通过一些 diff 算法来避免不必要的操作)。日常开发主要在于 Widget Tree 的构建工作,而 flutter 的 widget 主要分为StatelessWidget 和 StatefulWidget 两种,前者是一种无状态的控件,用于展示静态内容,而后者则是动态的,数据通过状态的形式与界面发生交互逻辑。而该空间的数据有且只有通过 setState 方式才能触发界面的更新(为 Element 节点设置 dirty 标志位,调用 build 方法)。

如果在 Widget Tree 的两个分支都需要用到同一个 state 时麻烦的点就来,我们需要将 state 定义在两个分支的根节点,然后需要将 state 沿着树的分支往下传递到目标节点。当层次较深时,这种方式显然是不能接受的。如图所示

flutter_passing

InheritedWidget

该 widget 能将数据沿着 widget tree 高效地向下传递,通过定义静态方法 InheritedWidget.of(context) 方法可以获取到唯一实例,其实是通过上下文 context. inheritFromWidgetOfExactType 方法来取。
以一个简单的定义为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class FrogColor extends InheritedWidget {
const FrogColor({
Key key,
@required this.color,
@required Widget child,
}) : assert(color != null),
assert(child != null),
super(key: key, child: child);

final Color color;

static FrogColor of(BuildContext context) {
return context.inheritFromWidgetOfExactType(FrogColor);
}

@override
bool updateShouldNotify(FrogColor old) => color != old.color;
}

通过定义的静态方法 of,我们可以通过 BuildContext.inheritFromWidgetOfExactType 方法在同一个上下文环境(同一颗 widget tree)中得到同一个实例。另外我们也可以在静态方法中实现我们自己的逻辑,比如说直接获取到 color 或者做一个空判断的逻辑等等

flutter_inherited

在 flutter 中,视图的更新需要调用 StatefulWidget 的 setState 方法,那么视图应该如何响应通过 InheritedWidget 传递的数据的变化呢,下面有几种方案可以参考实施

ScopedModel

在 pubspec.yaml 中引入 scoped_model 包

  • 定义自己的数据 model 继承自 Model 类,在数据改变时调用 notifyListeners() 方法
1
2
3
4
5
6
7
8
9
10
class MyModel extend Model{
final Data _data = Data();

Output get output => data.output;

void input(Input input){
...data 处理 input
notifyListeners();
}
}
  • 在 widget tree 中插入一个 ScopedModel 的 widget,将数据类实例赋值给 model 变量
1
2
3
4
5
6
Widget build(BuildContext context) {
return ScopedModel<CartModel>(
model: MyModel(),
child: ...,
);
}
  • 在 widget tree 需要调用数据的地方使用 ScopedModelDescendant,通过 Widget ScopedModelDescendantBuilder(BuildContext context, Widget child, T model) 方法构造 child widget
1
2
3
ScopedModelDescendant<CartModel>(
builder: (context, child, model) => ...model.output获取数据
)
  • 对于事件源,同样需要获取到 model 实例,对于事件源不需要刷新界面的场景,需要对 rebuildOnChange 参数设置,当该参数为 false 时,界面不会响应数据的变化
1
2
3
4
ScopedModelDescendant<CartModel>(
rebuildOnChange: false,
builder: (context, child, model) => ...model.input(input)传入事件
)

原理

ScopedModel 继承自 StatelessWidget,但他的 build 方法构造了两个关键的 widget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ScopedModel<T extends Model> extends StatelessWidget {

final T model;

final Widget child;

ScopedModel({this.model, this.child}) : assert(model != null);

@override
Widget build(BuildContext context) => new _ModelListener(
model: model,
builder: (BuildContext context) => new _InheritedModel<T>(
model: model,
child: child,
),
);
}

_ModelListener 是一个继承自 StatefulWidget 的控件,model 变化通过 notifyListeners 方法传递到该控件中触发 setState 操作,从而调用 builder 响应变化。而 _InheritedModel 则继承自 InheritedWidget 用于传递数据。

而 ScopedModelDescendant 内部会通过 ModelFinder 类去通过上下文去获取 _InheritedModel 传递的实例将数据绑定到界面上,当 rebuildOnChange = false 时,会通过 _InheritedModel.ancestorWidgetOfExactType(type) 获取数据,界面不会响应数据的变化

Bloc & Stream(rxdart版)

Bloc 指 Business logic,用于承载逻辑,通过 stream 响应式编程实现。Android 开发小伙伴肯定熟悉 RxJava,在 flutter 中我们同样可以通过 rxdart 来实现响应式数据流

  • 定义数据类,然后定义 Bloc 类,创建 StreamController 作为被观察者,创建 Subject 作为订阅者,通过 StreamController.stream.listen 产生订阅关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class MyBloc {
final Data = Data();

final StreamController<Input> _inputController =
StreamController<Input>();

final BehaviorSubject<Output> _outputSubject =
BehaviorSubject<Output>(seedValue: output);

MyBloc() {
_inputController.stream.listen((input) {
//你的转换逻辑
Output output = xxx(input);
_outputSubject.add(output);
});
}

Stream<Output> get outputStream => _output.stream;

Sink<Input> get inputSink => _inputController.sink;

void dispose() {
_output.close();
_inputController.close();
}
}
  • 定义 Provider 继承 InheritedWidget 作为获取 Bloc 实例的路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyBlocProvider extends InheritedWidget {
final MyBloc myBloc;

MyBlocProvider({
Key key,
MyBloc myBloc,
Widget child,
}) : bloc = myBloc ?? MyBloc(),
super(key: key, child: child);

@override
bool updateShouldNotify(InheritedWidget oldWidget) => true;

static Bloc of(BuildContext context) =>
(context.inheritFromWidgetOfExactType(MyBlocProvider) as MyBlocProvider)
.myBloc;
}
  • 事件源可以直接通过 Provider.of(context) 获取到的实例对 StreamController.sink 添加事件
1
2
3
4
5
Widget build(BuildContext context) {
final myBloc = MyBlocProvider.of(context);
...
myBloc.inputSink.add(event);
}
  • 界面响应则需要 StreamBuilder 进行包裹,需要添加 stream 流,为 Bloc 中 Subject.stream,通过 Widget AsyncWidgetBuilder(BuildContext context, AsyncSnapshot snapshot) 方法是界面响应数据变化
1
2
3
4
5
6
7
8
Widget build(BuildContext context) {
final myBloc = MyBlocProvider.of(context);
return StreamBuilder<int>(
stream: myBloc.output,
initialData: ...,
builder: (context, snapshot) => ...通过 snapshot.data 获取数据
);
}

flutter_blo

原理

StreamBuilder 继承自 StreamBuilderBase, 而
它又继承自 StatefulWidget,在 _StreamBuilderBaseState 中有对应的更新逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class _StreamBuilderBaseState<T, S> extends State<StreamBuilderBase<T, S>> {
StreamSubscription<T> _subscription;
S _summary;

@override
void initState() {
super.initState();
_summary = widget.initial();
_subscribe();
}

@override
void didUpdateWidget(StreamBuilderBase<T, S> oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.stream != widget.stream) {
if (_subscription != null) {
_unsubscribe();
_summary = widget.afterDisconnected(_summary);
}
_subscribe();
}
}

@override
Widget build(BuildContext context) => widget.build(context, _summary);

@override
void dispose() {
_unsubscribe();
super.dispose();
}

void _subscribe() {
if (widget.stream != null) {
_subscription = widget.stream.listen((T data) {
setState(() {
_summary = widget.afterData(_summary, data);
});
}, onError: (Object error) {
setState(() {
_summary = widget.afterError(_summary, error);
});
}, onDone: () {
setState(() {
_summary = widget.afterDone(_summary);
});
});
_summary = widget.afterConnected(_summary);
}
}

void _unsubscribe() {
if (_subscription != null) {
_subscription.cancel();
_subscription = null;
}
}
}

在 state 中维护了一个订阅关系 StreamSubscription,传入的 stream 流会在 initState 以及 didUpdateWidget 方法中通过 _subscribe 方法被订阅。当 stream 中传递了新的数据时就会触发 widget 的 setState 方法去更新 _summary 的值即我们的 AsyncSnapshot 使用的值从而触发界面的更新

与 ScopedModel 比较

由于 ScopedModel 是在最顶层的 ScopedModel 中去响应数据的改变,虽然 flutter 内部有相应的 diff 操作来避免额外的界面刷新,但还是有些过重,同样由于内部集成了_InheritedModel 导致需要 rebuildOnChange 去阻止一些类似事件源的非必要刷新。
而 StreamBuilder 则只是在对应的节点进行刷新操作,而且对于响应式编程中数据的操作更加的灵活,代码也更加的简洁

Redux

想必开发过 rn 项目的小伙伴对于 redux 这种架构并不陌生吧,作为一个响应式界面开发框架,flutter 也有第三方的 flutter_redux 实现

在 pubspec.yaml 中引入 flutter_redux 包

  • 定义数据类,然后定义好 Action 以及对应的 Reducer
1
2
3
4
5
6
class InputAction {

InputAction(this.input);

final Input input;
}
  • 创建 Store 类实例,传入 Reducer 和初始化的 state 状态,在 widget tree 中插入 StoreProvider\,并将创建好的 store 实例传入
1
2
3
4
5
6
7
Data myReducer(Data state, dynamic action) {
if (action is InputAction) {
// Reducer always returns a state,never mutating the old
return Data.clone(state)..input(action.input);
}
return state;
}
1
2
3
4
5
6
7
8
9
final store = Store<Data>(myReducer, initialState: Data());

@override
Widget build(BuildContext context) {
return StoreProvider<Cart>(
store: store,
child: ...
);
}
  • 在 widget tree 中通过 StoreConnector\<Store, ViewModel> 来构造界面,通过 converter 函数实现 Store 到 ViewModel 的转换(可以是获取 state 数据,也可以是获取 dispatch 的 callback 函数),再通过 builder 函数生成界面

界面

1
2
3
4
StoreConnector<Data, Output>(
converter: (store) => store.state.output,
builder: (context, output) => ...通过 output 获取数据
)

事件源

1
2
3
4
5
6
StoreConnector<Data, Function(Input)>(
// Dispatch the product to the reducer somehow
converter: (store) =>
(input) => store.dispatch(InputAction(input)),
builder: (context, callback) => ...通过 callback 来触发事件
)

flutter_redux

原理

StoreProvider 继承自 InheritedWidget,用于存储 store 实例。而 StoreConnector 继承自 StatelessWidget 但是会返回 _StoreStreamListener 的继承自 StatefulWidget 的 widget,内部则是通过 StreamBuilder 去实现响应流的,获取的 stream 来自于 store.onChange 即内部变量 _changeController.stream

store.dispatch 方法有如下处理

1
2
3
4
5
6
7
8
9
10
NextDispatcher _createReduceAndNotify(bool distinct) {
return (dynamic action) {
final state = reducer(_state, action);

if (distinct && state == _state) return;

_state = state;
_changeController.add(state);
};
}

每次分发 action 时就会触发 stream 流

结语

对于一般的页面而言,InheritedWidget 构造出 DataProvider 足矣,但对于复杂页面的交互就需要 redux,bloc+stream 帮忙了。个人而言更倾向于 blco+stream 这种方式更加的灵活,加之日后生态成熟了以后一些扩展功能对于 rxdart 之类的响应式框架更加友好。而基于 stream 编写的 redux 则对于一些前端开发者更加友好,但确实扩展性稍显不足。
希望有想更深入了解可以去看一下前言t
自己的 io 演讲,以及对应的 github 项目。